package com.redfootdev.horseskills;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Entity;
import org.bukkit.metadata.FixedMetadataValue;
import org.bukkit.metadata.MetadataValue;
import org.bukkit.plugin.java.JavaPlugin;

import com.redfootdev.horseskills.Skill;

//
// Database - this is the super class for both horses and players.  It implements the common
//				routines for the two including adding/deleting entries and dumping/loading
//				the database from a file.
//

public class Database {
	
	// this is used as the root for all metadata sent to the Bukkit metadata for entities
	private static final String MetaDataRoot = "HorseSkills.";
	
	private ArrayList<Entity>	entityDB;		// the entities are are currently in this DB
	private ArrayList<String>	attributeNames;	// list of all of the attributes names for this database
	private JavaPlugin			plugin;
	private String				dbname;			// name of THIS DB
	private File				dbDir;			// the database directory for THIS DB
	private int					saveInterval;	// number of minutes before save of dirty items (zero means no interval save)
	
	// this enum defines the type of data that this database can store
	// the given names are used for the "type" configuration field
	
	private enum dataType {
		
		DB_INT("integer"),
		DB_LONG("long"),
		DB_STRING("string"),
		DB_SKILL("skill"),
		DB_INDEXED_INT("indexed-integer"),
		DB_INDEXED_LONG("indexed-long"),
		DB_INDEXED_STRING("indexed-string"),
		DB_INDEXED_SKILL("indexed-skill"),
		DB_UNKNOWN("unknown");
		
		private String name;
		
		dataType(String name) {
			this.name = name;
		}
		
		public String toString()
		{
			return(name);
		}
		
		public static dataType fromString(String name)
		{
			for(dataType d: dataType.values()) {
				if (d.name.equalsIgnoreCase(name)) {
					return(d);
				}
			}
			return(dataType.DB_UNKNOWN);
		}
	};
	
	//
	// Database() - (constructor)
	//
	protected Database(JavaPlugin plugin, String dbname)
	{
		this.plugin = plugin;
		this.dbname = dbname;
		entityDB = new ArrayList<Entity>();
		this.attributeNames = new ArrayList<String>();
		// the following used to set the dbDir right up front - but the problem is that the plugin isn't
		//   well defined during the call of this constructor, so it is set later.
		// dbDir = new File(plugin.getDataFolder(),dbname);
		dbDir = null;
		saveInterval = 0;
	}

	//
	// databaseName() - returns the name of the database used for setting metadata in
	//					an entity.
	//
	private String databaseName()
	{
		return(MetaDataRoot + dbname + ".");
	}
	
	//
	// longAttributeName() - returns the long attribute name given the short one.  Only
	//						  used by routines in this class.
	//
	private String longAttributeName(String attrName)
	{
		return(databaseName() + attrName);
	}
	
	private void setAttributeType(Entity entity, String name, dataType type)
	{
		String attrString = longAttributeName(name) + ".type";
		
		entity.setMetadata(attrString, new FixedMetadataValue(plugin,type));	
	}
	
	public void saveInterval(int i)
	{
		saveInterval = i;
	}
	//
	// getAttributeType() - gets the type of an attribute
	//
	private dataType getAttributeType(Entity entity, String name)
	{
		String attrString = longAttributeName(name) + ".type";

		List<MetadataValue> values = entity.getMetadata(attrString);  
		for(MetadataValue value : values) {
			return((dataType)(value.value()));
		}
		return(dataType.DB_UNKNOWN);
	}
	
	//
	// removeAttributeIndex() - 
	// addAttributeIndex() - when an indexed attribute is placed on an entity, that entity needs to have
	//						an index of all of those indexed entities.  The index must be kept as
	//						UUIDs because an indexed entity may not be "an entity" when it is loaded
	//						from the disk.
	//
	// getAttributeIndex()
	// setAttributeIndex() - The attribute index is a piece of metadata on the given entity, and is kept
	//						as an ArrayList of UUIDs.

	private void setAttributeIndex(Entity entity, String name, ArrayList<String> index)
	{
		String attrString = longAttributeName(name) + ".index";
		
		entity.setMetadata(attrString, new FixedMetadataValue(plugin,index));	
	}
	
	private ArrayList<String> getAttributeIndex(Entity entity, String name)
	{
		String attrString = longAttributeName(name) + ".index";
		
		if(entity.hasMetadata(attrString)) {
			List<MetadataValue> values = entity.getMetadata(attrString);  
			for(MetadataValue value : values) {
				return((ArrayList<String>)(value.value()));
			}
		} 
		return(new ArrayList<String>());
	}
	
	private void addAttributeIndex(Entity entity, String entityIndexedString, String name) {
		ArrayList<String> index = getAttributeIndex(entity,name);
		if(!index.contains(entityIndexedString)) {
			index.add(entityIndexedString);
			setAttributeIndex(entity,name,index);
		}
	}
	
	private void removeAttributeIndex(Entity entity, Entity entityIndexed, String name) {
		UUID targetUUID = entityIndexed.getUniqueId();
		ArrayList<String> index = getAttributeIndex(entity,name);
		if(index.contains(targetUUID)) {
			index.remove(targetUUID);
			setAttributeIndex(entity,name,index);
		}
	}

	
	//
	// setAttribute() - used by sub-classes, sets the given attribute for the given entity.
	//					The sub-classes don't care how it gets done...it just gets done.
	//					Also, this class keeps track of the attributes that were used and
	//					knows to dump them.
	//
	protected void setAttribute(Entity entity, String name, int value)
	{
		if(!attributeNames.contains(name)) {
			attributeNames.add(name);				// add this attribute to the list of attributes to dump
		}
		
		String attrString = longAttributeName(name);

		entity.setMetadata(attrString + ".value", new FixedMetadataValue(plugin,value));

		setAttributeType(entity,name,dataType.DB_INT);
		setDirty(entity);
		insertTracking(entity);
	}
	
	protected void setAttribute(Entity entity, String name, long value)
	{
		if(!attributeNames.contains(name)) {
			attributeNames.add(name);				// add this attribute to the list of attributes to dump
		}
		
		String attrString = longAttributeName(name);

		entity.setMetadata(attrString + ".value", new FixedMetadataValue(plugin,value));

		setAttributeType(entity,name,dataType.DB_LONG);
		setDirty(entity);
		insertTracking(entity);
	}
	protected void setAttribute(Entity entity, String name, String value)
	{
		if(!attributeNames.contains(name)) {
			attributeNames.add(name);				// add this attribute to the list of attributes to dump
		}

		String attrString = longAttributeName(name);

		entity.setMetadata(attrString + ".value", new FixedMetadataValue(plugin,value));

		setAttributeType(entity,name,dataType.DB_STRING);
		setDirty(entity);
		insertTracking(entity);
	}
	protected void setAttribute(Entity entity, String name, Skill value)
	{
		if(!attributeNames.contains(name)) {
			attributeNames.add(name);				// add this attribute to the list of attributes to dump
		}

		String attrString = longAttributeName(name);

		entity.setMetadata(attrString + ".value.level", new FixedMetadataValue(plugin,value.getLevel()));
		entity.setMetadata(attrString + ".value.XP", new FixedMetadataValue(plugin,value.getXP()));

		setAttributeType(entity,name,dataType.DB_SKILL);
		setDirty(entity);
		insertTracking(entity);
	}

	//
	// setAttribute() - this PARTICULAR attribute setting is for an "indexed" attribute.
	//					that is, the value is indexed by another entity (normally a player).
	//
	protected void setAttribute(Entity entity, String name, Entity indexEntity, Skill value)
	{
		setAttribute(entity,name,indexEntity.getUniqueId(),value);
	}
	protected void setAttribute(Entity entity, String name, UUID indexEntityUUID, Skill value)
	{
		setAttribute(entity,name,indexEntityUUID.toString(),value);
	}
	protected void setAttribute(Entity entity, String name, String uniqueString, Skill value)
	{
		if(!attributeNames.contains(name)) {
			attributeNames.add(name);				// add this attribute to the list of attributes to dump
		}

		String attrStringComplete = longAttributeName(name) + "." + uniqueString;
		
		entity.setMetadata(attrStringComplete + ".type",new FixedMetadataValue(plugin,dataType.DB_SKILL));
		entity.setMetadata(attrStringComplete + ".value.level", new FixedMetadataValue(plugin,value.getLevel()));
		entity.setMetadata(attrStringComplete + ".value.XP", new FixedMetadataValue(plugin,value.getXP()));
		
		addAttributeIndex(entity, uniqueString, name);
		setAttributeType(entity,name,dataType.DB_INDEXED_SKILL);
		setDirty(entity);
		insertTracking(entity);
	}

	//
	// getAttribute() - used by sub-classes, gets the given attribute from the given entity.
	//					If the attribute doesn't exist, null is returned for Strings and zero for ints.
	//					This routine is smart enough to check the on-disk database for the entity
	//					if it isn't in the current entity list.  When something isn't in the current
	//					entity list, that means a set() hasn't been called for it (or a get() for that
	//					matter).
	//
	protected int getAttributeInt(Entity entity, String name)
	{
		loadEntityIfNeeded(entity);		// load the entity from the disk database if we need to
		
		String attrString = longAttributeName(name) + ".value";

		List<MetadataValue> values = entity.getMetadata(attrString);  
		for(MetadataValue value : values) {
			return(value.asInt());
		}
		return(0);
	}
	protected long getAttributeLong(Entity entity, String name)
	{
		loadEntityIfNeeded(entity);		// load the entity from the disk database if we need to

		String attrString = longAttributeName(name) + ".value";

		List<MetadataValue> values = entity.getMetadata(attrString);  
		for(MetadataValue value : values) {
			return(value.asLong());
		}
		return(0);
	}
	protected String getAttributeString(Entity entity, String name)
	{
		loadEntityIfNeeded(entity);		// load the entity from the disk database if we need to

		String attrString = longAttributeName(name) + ".value";

		List<MetadataValue> values = entity.getMetadata(attrString);  
		for(MetadataValue value : values) {
			return(value.asString());
		}
		return(null);
	}
	protected Skill getAttributeSkill(Entity entity, String name)
	{
		loadEntityIfNeeded(entity);		// load the entity from the disk database if we need to

		Skill retskill = new Skill();
		String attrString = longAttributeName(name) + ".value";

		List<MetadataValue> values = entity.getMetadata(attrString + ".level");  
		for(MetadataValue value : values) {
			retskill.setLevel(value.asInt());
		}
		values = entity.getMetadata(attrString + ".XP");  
		for(MetadataValue value : values) {
			retskill.setXP(value.asInt());
		}
		 return(retskill);
	}

	//
	// getAttributeSkill() - this PARTICULAR one is an "indexed" skill get.
	//						the index is a separate entity (normally a player).
	//						NOTE that this routine doesn't update the index
	//						of indexed entities - only setting does that.
	//
	protected Skill getAttributeSkill(Entity entity, String name, Entity index)
	{
		return(getAttributeSkill(entity,name, index.getUniqueId()));
	}
	protected Skill getAttributeSkill(Entity entity, String name, UUID index)
	{
		return(getAttributeSkill(entity,name,index.toString()));
	}
	protected Skill getAttributeSkill(Entity entity, String name, String indexString)
	{
		loadEntityIfNeeded(entity);		// load the entity from the disk database if we need to

		Skill retskill = new Skill();
		String attrStringComplete = longAttributeName(name) + "." + indexString + ".value";

		List<MetadataValue> values = entity.getMetadata(attrStringComplete + ".level");  
		for(MetadataValue value : values) {
			retskill.setLevel(value.asInt());
		}
		values = entity.getMetadata(attrStringComplete + ".XP");  
		for(MetadataValue value : values) {
			retskill.setXP(value.asInt());
		}
		return(retskill);
	}
	
	//
	// hasAttribute() - used by sub-classes, returns true if the given attribute exists for the
	//					given entity, or false otherwise.
	//
	protected boolean hasAttribute(Entity entity, String name)
	{
		loadEntityIfNeeded(entity);		// load the entity from the disk database if we need to

		return(entity.hasMetadata(longAttributeName(name) + ".type"));
	}
	protected boolean hasAttribute(Entity entity, String name, Entity index)
	{
		return(hasAttribute(entity,name,index.getUniqueId()));
	}
	protected boolean hasAttribute(Entity entity, String name, UUID indexUUID)
	{
		return(hasAttribute(entity,name,indexUUID.toString()));
	}
	protected boolean hasAttribute(Entity entity, String name, String uniqueIndex)
	{
		return(hasAttribute(entity,name + "." + uniqueIndex));
	}
	
	//
	// setDirty() - set on an entity, determines if any attributes for that entity have changed.
	//				Since file dumping is done on a entity==file basis, then all attributes
	//				for an entity are dumped if any are.
	// setClean() - touched when an entry has been dumped, so it doesn't have to be dumped again
	//				until it changes.
	// isDirty()  - returns TRUE if it is dirty, FALSE otherwise.
	//
	private void setDirty(Entity entity)
	{
		String attrString = longAttributeName("dirty");
		entity.setMetadata(attrString, new FixedMetadataValue(plugin,true));		
	}

	private void setClean(Entity entity)
	{
		String attrString = longAttributeName("dirty");
		entity.setMetadata(attrString, new FixedMetadataValue(plugin,false));		
	}
	
	private boolean isDirty(Entity entity) {
		String attrString = longAttributeName("dirty");

		List<MetadataValue> values = entity.getMetadata(attrString);  
		for(MetadataValue value : values) {
			return(value.asBoolean());
		}
		return(false);
	}
		
	// insertTracking()
	// removeTracking()
	// beingTracked() - these routines maintain the in memory list of entities that have been used
	//				with this database.  This list is used to dump data (knows which entities to
	//				dump) when a sync() is called.
	//
	private void insertTracking(Entity entity) {
		if (!beingTracked(entity)) {
			if(!(entityDB.add(entity))) {
				plugin.getLogger().info("Can't add to Entity Database. SERIOUS ERROR");
			}
		}
	}
	protected void removeTracking(Entity entity) {
		if (beingTracked(entity)) {
			if(!(entityDB.remove(entity))) {
				plugin.getLogger().info("Can't remove from Entity Database. SERIOUS ERROR");
			}
		}
	}
	protected boolean beingTracked(Entity entity)
	{
		return(entityDB.contains(entity));
	}
	
	//
	// dump() - Write this database out to disk.  The database is organized as a single file
	//			per entity, which contains all of the attributes for that entity.  The directory
	//			used for the files is the same as that for the database.  One backup is kept of
	//			each of the files.
	//
	protected void dump() throws IOException 
	{
		if(dbDir == null) {
			dbDir = new File(plugin.getDataFolder(),dbname);			
		}
		
		if (!dbDir.exists()) {
			// the database directory doesn't exist - so create it.
			if(!dbDir.mkdir()) {
				return;					// silently fail - we just can't write this database out
			}
		}
		
		// now cruise through the existing entities and write/create those that have changed
		
		for (Entity e: entityDB ) {
			if(isDirty(e)) {
				dumpEntity(e);
			}
		}
	}

	//
	// dumpEntity() - this routine will dump the given entity off to disk.  There are a couple
	//					of important things to realize:
	//				1 - this is simply a transcription of metadata to disk.
	//				2 - for indexed attributes, it is possible that the indexed values are for
	//					entities (like players) who don't currently have entities (not logged
	//					in).  These attributes MAY still have values for those entities, because
	//					the values were loaded from disk.
	//				3 - each case below calls "getAttributeXX()" which calls loadEntityIfNeeded().
	//					But, because this routine is normally called from dump() which is enumerating
	//					the tracking database, the loadEntityIfNeeded() will never do anything.
	//					However, if this routine is somehow called for an entity that isn't currently
	//					being tracked, this set-up will do the right thing by first loading the entity.
	//					
	
	protected void dumpEntity(Entity entity) throws IOException
	{
		if(dbDir == null) {
			dbDir = new File(plugin.getDataFolder(),dbname);			
		}
		// note that unique identifier for an entity is its UUID - that's how we identify
		// the entities in the yml file for loading

		UUID uuid = entity.getUniqueId();
		
		File efile = new File(dbDir,uuid.toString());
		File efileBackup = new File(dbDir,uuid.toString() + "~");
			
		if(efile.exists()) {
			efile.renameTo(efileBackup);
		}

		// at this point we only write out those attributes that this entity
		// has, so we can safely ignore the existing file (ie - not reload it)
		// so this just creates a blank config

		FileConfiguration eConfig = YamlConfiguration.loadConfiguration(efile);
		
		// now, for each attribute that was stored for ANY entity, see if this
		// entity has it - and if so, dump it.
			
		for(String s : attributeNames) {
			if (hasAttribute(entity,s)) {
				
				eConfig.set(s+".type", getAttributeType(entity,s).toString());
				
				switch(getAttributeType(entity,s)) {

				case DB_INT:
					int ivalue = getAttributeInt(entity,s);
					eConfig.set(s + ".value", ivalue);
					break;
				
				case DB_LONG:
					long lvalue = getAttributeLong(entity,s);
					eConfig.set(s + ".value",lvalue);
					break;
				
				case DB_STRING:
					String svalue = getAttributeString(entity,s);
					eConfig.set(s + ".value", svalue);
					break;
				
				case DB_SKILL:
					Skill kvalue = getAttributeSkill(entity,s);
					eConfig.set(s + ".value.level", kvalue.getLevel());
					eConfig.set(s + ".value.XP", kvalue.getXP());
					break;
				
				case DB_INDEXED_SKILL:
					ArrayList<String> index = getAttributeIndex(entity,s);
					for(String iString : index) {
						Skill ikvalue = getAttributeSkill(entity,s,iString);
						eConfig.set(s + ".value." + iString + ".level", ikvalue.getLevel());
						eConfig.set(s + ".value." + iString + ".XP", ikvalue.getXP());
					}
					break;
				
				case DB_INDEXED_INT:
				case DB_INDEXED_LONG:
				case DB_INDEXED_STRING:
				case DB_UNKNOWN:
					plugin.getLogger().info("value for indexed " + s + " ignored");
					break;
				}
			}
		}
		// finally, write out the database
		eConfig.save(efile);
	}

	//
	// loadEntityIfNeeded() - checks to see if the current entity is being tracked.  If it
	//					isn't, try to load the entity from disk.  If it can't be
	//					loaded from disk, then no big deal.
	//
	protected void loadEntityIfNeeded(Entity entity)
	{
		if (!beingTracked(entity)) {
			// not currently tracking the given entity - try to load it from disk
			// if it doesn't work or even throws an error, we don't care
			loadEntity(entity);
		}
	}
	
	//
	// loadEntity() - given a particular entity, try to load the associated information for that
	//					entity for the current database.  Entities are identified by their UUID.
	//					So the UUID for the given entity is looked up in the disk database.
	//
	protected boolean loadEntity(Entity entity)
	{
		if(dbDir == null) {
			dbDir = new File(plugin.getDataFolder(),dbname);			
		}
		UUID uuid = entity.getUniqueId();
		
		File efile = new File(dbDir,uuid.toString());

		if(efile.exists()) {		// yippie! - we have this entity in the disk database, so load it
			loadFile(efile,entity);
			insertTracking(entity);
			return(true);
		}
		return(false);
	}
	
	//
	// destroy() - given a particular entity, permanently remove it from the database.  Given the
	//				"metadata" implementation of this database, this routine doesn't worry about
	//				the metadata assuming that the destroy was called on an entity that will be (or
	//				has been) removed from the server.  This routine DOES remove the entity from
	//				this object's list.
	//
	protected void destroy(Entity entity)
	{
		// TODO - finish this!  when entities are removed
	}
	
	// 
	// loadFile() - this routine is the inverse of dumpEntity().  It loads the given entity
	//					(given as a FILE) and sets all metadata appropriately.
	//
	
	private void loadFile(File efile, Entity entity)
	{
		FileConfiguration eConfig = YamlConfiguration.loadConfiguration(efile);
		
		// now we have a configuration - time to put it into the metadata

		// first get the type of the entry
		// then dump it
		
		for(String key: eConfig.getKeys(false)) {		// get the top level keys in the config file
			dataType d = dataType.fromString(eConfig.getString(key + ".type"));

			switch(d) {

			case DB_INT:
				int ivalue = eConfig.getInt(key + ".value");
				setAttribute(entity,key,ivalue);
				setClean(entity);
				break;
			
			case DB_LONG:
				long lvalue = eConfig.getLong(key + ".value");
				setAttribute(entity,key,lvalue);
				setClean(entity);
				break;
			
			case DB_STRING:
				String svalue = eConfig.getString(key + ".value");
				setAttribute(entity,key,svalue);
				setClean(entity);
				break;
			
			case DB_SKILL:
				Skill kvalue = new Skill();
				kvalue.setLevel(eConfig.getInt(key + ".value.level"));
				kvalue.setXP(eConfig.getInt(key + ".value.XP"));
				setAttribute(entity,key,kvalue);
				setClean(entity);
				break;
			
			case DB_INDEXED_SKILL:
				// this one is a bit tougher - need to loop through the SECTIONS in this attribute
				ConfigurationSection isection = eConfig.getConfigurationSection(key + ".value");
				for(String uniqueString : isection.getKeys(false)) {
					Skill ikvalue = new Skill();
					ikvalue.setLevel(isection.getInt(uniqueString + ".level"));
					ikvalue.setXP(isection.getInt(uniqueString + ".XP"));
					setAttribute(entity,key,uniqueString,ikvalue);
				}
				break;
			
			case DB_INDEXED_INT:
			case DB_INDEXED_LONG:
			case DB_INDEXED_STRING:
			case DB_UNKNOWN:
				plugin.getLogger().info("bad value type in config");
				break;
			}
		}
	}
}
